Peer-to-peer Git: Radicle Seed Node on OpenBSD
While Git is decentralized by design, in many cases it still depends on a classical server-client architecture. Many projects rely on GitHub, GitLab, or another centralized platform to host their repositories and thereby make them available to everyone. What if we could have Git, but without depending on any centralized servers at all, and instead use it peer-to-peer?
Even though Git is decentralized by design, in reality, it is very much centralized, with plenty of popular projects being in the hands of just a few organizations: GitHub (Microsoft), GitLab, Launchpad (Canonical), and Codeberg, just to name some the most recognized ones. If any of these organizations decide to boot a project – or worse, shut the whole service down – it will definitely lead to at least some headaches and inconveniences for maintainers, as well as users.
Ideally, the Git infrastructure that we use would instead also be fully decentralized, using a peer-to-peer architecture in which no repository lives on only a single server, but is being replicated by many different servers, hosted by different individuals and organizations. This is where Radicle comes in:
Radicle is an open source, peer-to-peer code collaboration stack built on Git. Unlike centralized code hosting platforms, there is no single entity controlling the network. Repositories are replicated across peers in a decentralized manner, and users are in full control of their data and workflow.
In this write-up we’ll be looking at how to set up our seed node, allowing us to not only replicate existing projects on Radicle but also host our repositories and make them available to the rest of the network.
Preparation
Usually, when I build an OpenBSD-based infrastructure, I use a Vultr VPS instance. You can start with the lowest VPS configuration and upgrade over time if necessary. Anything with at least 1GB of RAM and 10GB of storage should be fine.
We’re going to be using the fresh-out-the-oven OpenBSD 7.5 for this setup.
Note: I won’t explicitly mention under which user I’m running each command, hence please read carefully. When the prompt shows
seed#
, I’m acting asroot
user, otherwise, when it showsseed%
, I’m using the_seed
user.
As soon as the OpenBSD VPS has booted, we can log in via SSH and perform a quick update of the system:
seed# syspatch
seed# pkg_add -u
Next, perform a system upgrade to -current
. We want this because -stable
releases especially for Rust (which we will require for building Radicle v0.9.0)
are a bit too outdated:
seed# sysupgrade -s
The system will reboot and when you log in you should be greeted with a version
that says -current
:
OpenBSD 7.5-current (GENERIC) #23: Thu Apr 11 12:18:37 MDT 2024
Welcome to OpenBSD: The proactively secure Unix-like operating system.
After that, let’s do a quick upgrade of the existing packages and install some handy tools:
seed# pkg_add -uvi
seed# pkg_add git zsh neovim wget mosh rsync htop
What I like to do is link nvim
to vim
, because typing vim
is in my muscle
memory. I also like to use zsh
as shell. Since that’s all preference, these
steps are optional:
seed# ln -s /usr/local/bin/nvim /usr/local/bin/vim
seed# chsh -s /usr/local/bin/zsh root
Another optional but useful thing that I like to do is to change the SSH port
and disable password authentication / enable pubkey authentication. Make sure
you have an authorized_keys
entry with your pubkey in place before applying
this change:
seed# sed -i 's/^#Port 22/Port 31231/g;\
s/^#PubkeyAuthentication .*/PubkeyAuthentication yes/g;\
s/^#PasswordAuthentication .*/PasswordAuthentication no/g' \
/etc/ssh/sshd_config
seed# rcctl restart sshd
Afterwards, disconnect from SSH, and re-connect, ideally using mosh
, and
launch tmux
for the sake of comfort. See this
post on how to
make the mosh
experience even smoother.
Radicle
First, let’s install the required software to build Radicle:
seed# pkg_add rust
Then, install Radicle using cargo
, the Rust package manager:
seed# #https://github.com/rust-lang/cargo/issues/11435#issuecomment-1740163332
seed# ulimit -n 1024
seed# cargo install --force --locked \
--git https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git \
radicle-cli \
radicle-node \
radicle-remote-helper
seed# mv ~/.cargo/bin/* /usr/local/bin/
FYI: Depending on the performance of your machine, this process might take more than a cup of coffee in time. If you’re setting up a single vCPU VPS, consider running it on a beefier machine and transferring the built binaries to your server.
Normally we would also compile the radicle-httpd
package together with all the
others. Unfortunately, there’s currently an issue on OpenBSD that prevents it
from building:
Compiling radicle-term v0.9.0 (/root/.cargo/git/checkouts/z3gqcJUoA1n9HaHKufZs5FCSGazv5-cab4735f10a16009/574ac35/radicle-term)
error[E0425]: cannot find value `cmd_name` in this scope
--> radicle-httpd/src/commands/web.rs:200:36
|
200 | let mut cmd = Command::new(cmd_name);
| ^^^^^^^^ not found in this scope
error[E0425]: cannot find value `cmd_name` in this scope
--> radicle-httpd/src/commands/web.rs:215:71
|
215 | term::error(format!("Could not open web browser via `{cmd_name}`"));
| ^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `radicle-httpd` (lib) due to 2 previous errors
We can, however, manually fix that issue. For that, we need to clone the
heartwood
repository and patch a single file within the radicle-httpd
folder:
seed# git clone https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git
seed# cd z3gqcJUoA1n9HaHKufZs5FCSGazv5/
seed# git diff radicle-httpd/
diff --git a/radicle-httpd/src/commands/web.rs b/radicle-httpd/src/commands/web.rs
index 3229ebf5..fb246539 100644
--- a/radicle-httpd/src/commands/web.rs
+++ b/radicle-httpd/src/commands/web.rs
@@ -196,6 +196,8 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let cmd_name = "open";
#[cfg(target_os = "linux")]
let cmd_name = "xdg-open";
+ #[cfg(target_os = "openbsd")]
+ let cmd_name = "echo";
let mut cmd = Command::new(cmd_name);
match cmd.arg(auth_url.as_str()).spawn() {
We can then proceed to manually build and install the binary for
radicle-httpd
:
seed# cd radicle-httpd
seed# cargo build --release
seed# mv ../target/release/rad-web ../target/release/radicle-httpd /usr/local/bin
Next, we create the _seed
group and user on the system:
seed# groupadd _seed
seed# useradd -d /home/_seed -m -c "Radicle Seed" \
-g _seed -L daemon -s /sbin/nologin _seed
After that, we can log in as _seed
user and configure Radicle:
seed# su -l -s /usr/local/bin/zsh _seed -
seed% /usr/local/bin/rad auth --alias seed.domain.com
As seed nodes do not typically sign permanent artifacts with their key, it is not generally necessary to set up a passphrase, so we can simply skip the prompt by pressing enter.
Now, let’s open the Radicle node configuration to adjust it:
seed% /usr/local/bin/rad config edit
First of all, we need to decide what seeding policy we’d like our seed to have. We can decide between permissive and selective seeding. To understand the differences, let’s have a quick look at the official Radicle manual:
A permissive or “open” policy is said to be fully-replicating, meaning your seed will try to have a fully copy of all repository data available on the network.
A selective or restricted policy requires you, the operator, to manually allow repositories to be seeded. This means that the node will ignore all repositories, except the ones that are pre-configured to allow seeding.
Tl;dr: If you’re looking to simply host a public seed to support the Radicle network, you’d want to go with a permissive policy. If, however, you’re looking to build more of a private seed with only your repositories, or repositories you care about, the selective policy is what you’d want.
For the permissive policy, configure the following values:
{
"node": {
...
"policy": "allow",
"scope": "all"
}
}
For the selective policy, configure the following values instead:
{
"node": {
...
"policy": "block",
"scope": "all"
}
}
Next, we configure the node’s external address with a subdomain/domain that points to the server and for which we’re later on going to create an SSL certificate:
{
"node": {
...
"externalAddresses": ["seed.domain.com:8776"]
}
}
Now, we create the rc.d
files and adjust the permissions:
seed# cat /etc/rc.d/radicle
#!/bin/ksh
daemon="/usr/local/bin/rad"
daemon_flags="node start -- --listen 0.0.0.0:8776"
daemon_user="_seed"
. /etc/rc.d/rc.subr
rc_cmd $1
seed# chmod 555 /etc/rc.d/radicle
seed# cat /etc/rc.d/radiclehttpd
#!/bin/ksh
daemon="/usr/local/bin/radicle-httpd"
daemon_flags="--listen 127.0.0.1:8080"
daemon_user="_seed"
. /etc/rc.d/rc.subr
pexp="$daemon"
rc_bg=YES
rc_cmd $1
seed# chmod 555 /etc/rc.d/radiclehttpd
Make sure to enable and start the services:
seed# rcctl enable radicle
seed# rcctl start radicle
seed# rcctl enable radiclehttpd
seed# rcctl start radiclehttpd
We can test that radicle-httpd
is working now:
seed# curl http://127.0.0.1:8080/api/v1
SSL
To get an SSL certificate (from Let’s Encrypt) we’ll use OpenBSD’s
acme-client
and set up its configuration, as well as a cron job that will make
sure our certificate gets prolonged automatically.
First, create the file /etc/acme-client.conf
with the following content:
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/ssl/private/letsencrypt.key"
}
domain seed.domain.com {
domain key "/etc/ssl/private/seed.domain.com.key"
domain certificate "/etc/ssl/seed.domain.com.crt"
domain full chain certificate "/etc/ssl/seed.domain.com.pem"
sign with letsencrypt
}
Make sure to replace seed.domain.com
with the (sub)domain you pointed to your
cloud instance. Next, we’ll need to configure httpd to run on port 80
and
provide access to the ACME challenge. Create the file /etc/httpd.conf
with
the following content:
server "seed.domain.com" {
listen on * port 80
root "/htdocs/seed.domain.com"
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
}
Additionally, create the folder /var/www/htdocs/seed.domain.com/
and place an
empty index.html
into it.
Let’s enable httpd:
znc# rcctl enable httpd
znc# rcctl restart httpd
We can now try to issue our SSL certificate by running the following command:
znc# acme-client -v seed.domain.com
Your newly issued SSL certificate should be available under
/etc/ssl/seed.domain.com.{crt,pem}
and /etc/ssl/private/seed.domain.com.key
.
Next, run crontab -e
. This will open the current crontab for editing. Add the
following line at the very end of the file:
0 0 * * * acme-client -v seed.domain.com && rcctl
restart relayd
This will make sure our SSL certificate won’t expire.
relayd
Next we set up relayd
as a reverse proxy for the Radicle HTTP backend.
relayd
takes care of handling SSL termination. We’re going to use the
following configuration under /etc/relayd.conf
:
table <radicle-default-host> { 127.0.0.1 }
http protocol radicle-https {
match request header append "X-Real-IP" value "$REMOTE_ADDR"
match request header append "Host" value "$HOST"
match request header append "X-Forwarded-For" value "$REMOTE_ADDR"
match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match request path "/*" forward to <radicle-default-host>
tcp { nodelay, sack, backlog 128 }
tls keypair seed.domain.com
tls { no tlsv1.0, ciphers HIGH }
}
relay radicle-https-relay {
listen on egress port 443 tls
protocol radicle-https
forward to <radicle-default-host> port 8080
}
In /etc/rc.conf.local
change relayd_flags
to `` (empty). Then launch relayd:
seed# rcctl enable relayd
seed# rcctl start relayd
We can now verify that the API (radicle-httpd
) is available over the internet:
your-computer$ curl https://seed.domain.com/api/v1
In addition, we can also verify that the Radicle web app can access the node by opening the following link in our web browser:
https://app.radicle.xyz/nodes/seed.domain.com/
And that’s about it. If you have chosen the selective policy for your node,
you might now go ahead and start picking repositories that you want to seed
using the rad seed <uri>
command, e.g.:
seed% /usr/local/bin/rad seed rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
To remove a seeded repository, use the /usr/local/bin/rad unseed <uri>
command. Make sure to run these commands as the _seed
user.
Hint: Make sure to always specify the full path (
/usr/local/bin/rad
), otherwise you end up calling OpenBSD’s router advertisement daemon (/usr/sbin/rad
). You can also adjust yourPATH
for/usr/local/bin
to take precedence over/usr/sbin/
.
If you’d like to collaborate with me on Radicle, check out the projects on my seed and feel free to sync with it:
z6MksHwp2VUdawHNWNnd8YwDEkP1E6LuERxCGvTHHarEjYPi@seed.xn--gckvb8fzb.com:8776
Enjoyed this? Support me via Monero, Bitcoin, Lightning, or Ethereum! More info.